#region license
# This file is part of Grawlix.
# 
# Grawlix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
# 
# Grawlix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public
# License along with Grawlix.  If not, see
# <http://www.gnu.org/licenses/>.
#endregion

"""A caching base class for a resource factory."""

namespace Grawlix.Services.Resources
import Grawlix.Services
import C5
import System
import System.Threading

abstract class AbstractResourceFactory[of
	T/*(interface, IDynamicResource)*/](
	ServiceBase,
	IResourceFactory[of T]):
"""Convenience base class for resource factories."""
	
	protected def constructor(
		[required] uriResolver as IUriResolver):
	"""
	Registers needed services.
	Param uriResolver: A service used to complete URIs.
	"""
		_uriResolver = uriResolver
		_repository = ResourceRepository[of T](
			# FIXME: Cast needed for bug in Boo generics
			self as AbstractResourceFactory[of T])
		
	protected virtual def StartLoad(identifier as Uri):
	"""
	Loads the state for the given identifier.
	Param identifier: The URI of the resource to load.
	"""
		pass
	
	protected virtual def CancelLoad(identifier as Uri):
	"""
	Cancels an active load of the specified resource.
	Param identifier: The URI of resource to unload.
	"""
		pass
	
	protected virtual def StartUnload(state):
	"""
	Unloads the given resource.
	Param state: The resource state information, which may be null.
	"""
		pass
	
	protected virtual def UpdateState(elapsed as timespan):
	"""
	Updates the factory.
	Param elapsed: The amount of time since the last frame.
	"""
		pass
	
	def Find([required] identifier as Uri):
	"""
	Find and return a specified resource.
	Param identifier: The URI of the resource to locate.
	Returns: The new or cached resource.
	"""
		# HACK: This approach means verbose errors on every find.
		# If two factories are created.
		# Problem is that the factory needs to be created before
		# creating the cache.
		localCache = ResourceCache[of T].FindCurrentCache(_repository)
		# FIXME: Cast required temporarily
		return localCache.MarshalResource(identifier) as T
	
	[lock(_stateCache)]
	def Reload([required] resource as T):
	"""
	Reload the given resource.
	Param resource: The resource to reload.
	"""
		# FIXME: Cast required temporarily
		identifier = (resource as IDynamicResource).Identifier
		assert _stateCache.Contains(identifier)
		entry = _stateCache[identifier]
		if entry.Loading:
			CancelLoad(identifier)
		else:
			entry.Loading = true
		StartLoad(identifier)
	
	[lock(_stateCache)]
	def Update(elapsed as timespan):
	"""
	Runs an update on the factory.
	Param elapsed: The amount of time since last frame.
	"""
		Thread.VolatileWrite(_allowSet, 1)
		UpdateState(elapsed)
		Thread.VolatileWrite(_allowSet, 0)
	
	[lock(_stateCache)]
	# FIXME: Boo bug requires this be not internal
	/*internal */def RequestUpdate(
		[required] appDomain as AppDomain,
		[required] destination as ResourceCache[of T],
		[required] identifier as Uri):
	"""
	Requests an updated set of data to the destination.
	Param appDomain: The referencing AppDomain.
	Param destination: The cache from the referencing AppDomain.
	Param identifier: The URI of the resource.
	Returns: The marshalled resource.
	"""
		if _stateCache.Contains(identifier):
			entry = _stateCache[identifier]
		else:
			entry = ResourceEntry[of T]()
			_stateCache[identifier] = entry
			entry.Loading = true
			StartLoad(identifier)
		entry.AddEnvironment(appDomain, destination)
		unless _domainSet.Contains(appDomain):
			_domainSet.Add(appDomain)
			DomainUnloadCleaner[of T](
				# FIXME: Cast needed for bug in Boo generics
				self as AbstractResourceFactory[of T],
				appDomain)
		destination.UpdateState(
			identifier,
			ResourceState(
				StateObject: entry.State,
				LoadFailed: entry.LoadFailed))
	
	protected def SetState(
		[required] identifier as Uri,
		state,
		loadFailed as bool):
	"""
	Broadcasts a state change up the hierarchy.
	Param identifier: The URI of the resource.
	Param state: The resource state.
	Param loadFailed: Whether or not the load has failed.
	Remarks: This function may call other virtual functions.
	"""
		if Thread.VolatileRead(_allowSet) != 1:
			raise InvalidOperationException(
				"Can only update a factory's state during " +
				"the duration of the Update call. " +
				"Ensure that SetState is only called when " +
				"nested in Update.")
		unless _stateCache.Contains(identifier):
			raise ArgumentException(
				"The resource repository for ${typeof(T).Name} " +
				"has no entry for ${identifier}. " +
				"Ensure that only valid resources are updated.",
				"identifier")
		if state is not null and loadFailed:
			raise ArgumentException(
				"Received a report that the load failed, but " +
				"still given a state object. " +
				"Ensure that either [state] is null or " +
				"[loadFailed] is false.",
				"state")
		entry = _stateCache[identifier]
		if entry.State is not null:
			StartUnload(entry.State)
		entry.State = state
		entry.LoadFailed = loadFailed
		entry.Loading = false
		resourceState = ResourceState(
			StateObject: state,
			LoadFailed: loadFailed)
		for resourceCache in entry.CacheList:
			# AppDomain unload in progress could trigger exception?
			resourceCache.UpdateState(identifier, resourceState)
	
	[lock(_stateCache)]
	# FIXME: Boo bug requires this be not internal
	/*internal */def RequestRemoval(
		[required] domain as AppDomain,
		[required] identifier as Uri):
	"""
	Removes a reference to a resource.
	Param domain: The referencing domain.
	Param identifier: The URI of the resource.
	"""
		return unless _stateCache.Contains(identifier)
		entry = _stateCache[identifier]
		return \
			unless entry.GetEnvironment(domain).ResourcePresent(identifier)
		UnloadImpl(identifier, entry, domain)
	
	[lock(_stateCache)]
	# FIXME: Boo bug requires this be not internal
	/*internal */def DomainUnload([required] domain as AppDomain):
	"""
	Unload an entire AppDomain.
	Param domain: The domain to unload.
	"""
		_domainSet.Remove(domain)
		for pair in _stateCache.ToArray():
			UnloadImpl(pair.Key, pair.Value, domain)
	
	private def UnloadImpl(
		identifier as Uri,
		entry as ResourceEntry[of T],
		domain as AppDomain):
	"""
	Removes a reference to a resource.
	Param identifier: The URI of the resource.
	Param entry: The resource entry in the cache.
	Param domain: The referencing domain.
	"""
		entry.RemoveEnvironment(domain)
		if entry.IsEmpty:
			_stateCache.Remove(identifier)
			if entry.Loading:
				CancelLoad(identifier)
			if entry.State is not null:
				StartUnload(entry.State)
	
	override def ToString():
	"""
	Get a string representation for this object.
	Returns: A string representation for this object.
	"""
		return "${GetType().Name}"
	
	# FIXME: Boo bug requires this be public
	/*internal protected */abstract Creator as \
		AbstractResourceProxyCreator[of T]:
	"""Object which creates new resource proxies."""
		get:
			pass
	
	[getter(UriResolver, Protected: true)]
	private _uriResolver as IUriResolver
	"""A translator for URIs."""
	
	private _repository as ResourceRepository[of T]
	"""The internal data store."""
	
	private _stateCache = \
		HashDictionary[of Uri, ResourceEntry[of T]]()
	"""The cache of all state information."""
	
	private _domainSet = HashSet[of AppDomain]()
	"""The set of active AppDomains."""
	
	private _allowSet as int
	"""Whether or not to allow SetState operations."""

class ResourceState:
"""A simple tuple of object, bool."""
	
	[property(StateObject)]
	private _state
	"""The object state."""

	[property(LoadFailed)]
	private _loadFailed as bool
	"""Whether the load has failed."""

internal transient class DomainUnloadCleaner[of 
	T/*(interface, IDynamicResource)*/]:
"""An object that cleans out the cache when an AppDomain is unloaded."""

	def constructor(
		[required] factory as AbstractResourceFactory[of T],
		[required] appDomain as AppDomain):
	"""
	Initializes the cleaner.
	Param factory: The factory to send events to.
	Param appDomain: The triggering AppDomain.
	"""
		_factory = factory
		_appDomain = appDomain
		appDomain.DomainUnload += OnUnload
	
	def OnUnload(sender as object, e as EventArgs):
	"""
	Responds to the AppDomain.DomainUnload event.
	Param sender: The event sender (unused).
	Param e: The event arguments (unused).
	"""
		_factory.DomainUnload(_appDomain)
	
	_factory as AbstractResourceFactory[of T]
	"""The resource factory."""
	
	_appDomain as AppDomain
	"""The triggering AppDomain."""

internal transient class ResourceEntry[of 
	T/*(interface, IDynamicResource)*/]:
"""
An entry keeping track of the different
AppDomains referencing the object.
"""
	def AddEnvironment(
		[required] appDomain as AppDomain,
		[required] cache as ResourceCache[of T]):
	"""
	Add an environment reference to this entry.
	Param appDomain: The environment to add.
	Param cache: The resource cache for the environment.
	"""
		_appDomains.Add(appDomain, cache)
	
	def RemoveEnvironment([required] appDomain as AppDomain):
	"""
	Remove an environment reference from this entry.
	Param appDomain: The environment to remove.
	Remarks: May occur twice.
	"""
		_appDomains.Remove(appDomain)
	
	def GetEnvironment([required] appDomain as AppDomain):
	"""
	Remove an environment reference from this entry.
	Param appDomain: The environment to look up.
	Returns: The cache.
	"""
		return _appDomains[appDomain]
	
	CacheList:
	"""The list of the caches."""
		get:
			return _appDomains.Values
	
	IsEmpty:
	"""Whether or not the list of AppDomains is empty."""
		get:
			return _appDomains.IsEmpty
	
	[property(State)]
	private _state as object
	"""The proxy object kept track of, when _appDomains is not empty."""
	
	[property(LoadFailed)]
	private _loadFailed as bool
	"""Whether or not the load has failed."""
	
	[property(Loading)]
	private _loading as bool
	"""Whether the resource is currently loading."""
	
	private _appDomains = \
		HashDictionary[of AppDomain, ResourceCache[of T]]()
	"""The list of environments caching the resource."""
	
